引文原版:http://www.raywenderlich.com/73602/dynamic-table-view-cell-height-auto-layout
转载请注明出处:https://blog.caver.cc/2015/01/04/2015-01-4-Dynamic-TableViewCell-Height-By-Auto%20Layout/

11/23/2014: 已经更新兼容iOS7,iOS8和Xcode6.1

如果你在过去想创建一个自定义的表视图来完成动态适配表视图单元格的高度,你必须写很多的计算代码。你必须计算每一个label,image view,text field,以及其他一切单元格内手动创建的控件高度。
坦率地说,这是非常艰巨的,也是流水账,而且还容易出错。
在这个表视图单元格动态高度教程中,您将学习如何创建自定义单元格,并动态调整它们的大小,以适应它们的内容。如果你之前已经做过自定义单元格的工作,你可能在想,“这将需要大量的适配计算代码。”
但是,如果我们告诉你,你一点也不用写任何适配计算代码呢?
你可能会说“谬论!”。但是,你的确可以做到!
通过你在本教程结束的时候,你就会知道如何利用自动布局来搞定你的几百行代码。

注:本教程适用于iOS7及以上。没有使用只有iOS8才支持的自动计算tableview cell的api。

本教程假设你有基本的了解,你可以同时使用自动布局和UITableView中(包括它的dataSourcedelegate方法)。如果你需要在这之前刷新一下你的脑容量,那我就给你一百块!

那就开始吧

iOS6推出了精彩的新技术:自动布局。开发者们欢欣鼓舞,吃着火锅,唱着歌,出门就被干翻在地了。。。
好了,这些都说早了,自动布局,我们有一个大问题需要面对。
当它希望忽悠无数开发者,最初,Autolayout使用起来非常的笨重。手动编写自动布局代码,到现在仍然是对累赘的Objective-C中很好的解决办法,Interface Builder希望通过约束条件**(constraints)**来助人为乐往往适得其反。

随着Xcode5.1和iOS7的到来,Interface Builder对自动布局的支持有了质的飞跃。除此之外,iOS7中引入了一个非常重要的委托方法UITableViewDelegate:

1
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath;

该方法允许表视图**(TableView)**通过懒加载的方式来计算cell实际的高度,甚至可以把计算推迟到cell将要显现的时候。自动布局终于可以搞定动态计算单元格高度这种繁重的活了。
但是,你不想现在就掉进理论那大坑,我说的对不?你是否开始准备老老实实的编码了!所以让我们言归正传吧。

教程总览

试想一下,你的首要客户跑来对你逼逼:“我们的用户都吵着要用一种方式来查看自己喜欢的Deviant Artists的意见书”。
你有听过这种艺术吗?槽。。。
“嗯,这是一个流行的社交网络平台,让艺术家们可以分享他们的艺术创作,被称为叛逆博客。”你的客户又逼逼:“你真的应该看看Deviant Art网站。”
幸运的是,Deviant Art提供了一个RSS订阅入口,您可以通过艺术家来访问这些帖子。
“我们开始制作的应用程序,但我们被如何显示在表视图中显示正确内容难倒了,”你的客户怀疑。 “你能搞的定吗?”
现在,你什么都特么不用多考虑,只需要工具来搞定它,这样你的客户就会觉得你屌爆了或者屌翻了。

  • (老外都喜欢叨逼叨半天才开始,妹的,最烦翻译这些扯鸡巴蛋的东东)*

首先下载“客户端的代码”(本教程开始的工程)在这里

  • (老外除了会扯鸡巴蛋以外,让我最想亲近他们菊花的原因就在这里,太特么上手了,该替我们省的统统省掉,擦拉黑)*

本项目采用CocoaPods,所以双击打开DeviantArtBrowser.xcworkspace(而不是.xcodeproj文件)。压缩包里已经包含了依赖库,根本不需要你去开终端来pod install神马的,就是这么跩。

什么?你个逼搞了这么就iOS,都会搜索TableViewCell heightAutolayout这种关键词了还不知道CocoaPods?可以滚犊子了好伐。算了,还是一起给你吧,牛宝宝

下载完了就速度把工程打开,别磨磨唧唧的,WTF,run了一下什么内容都没,逗我玩呢?(都特么告诉你是起始工程,想什么玩意儿啊)

首先打开Main.storyboardViews分组下),看见所有视图米:

从左到右:

  • 顶级导航控制器
  • 标题(title)为Deviant Browser的RWFeedViewController
  • RWDetailViewController的两个场景(上面那个显示文字内容,下面那个显示文字和图片),标题分别为Deviant Article和Deviant Media

带你装逼带你飞,然我们跑起来,点击运行。你会看到控制台日志输出,除了输出就没干别了,除非你个煞笔改了代码。

 2014-05-28 00:52:01.588 DeviantArtBrowser[1191:60b] GET 'http://backend.deviantart.com/rss.xml?q=boost%3Apopular'
 2014-05-28 00:52:03.144 DeviantArtBrowser[1191:60b] 200 'http://backend.deviantart.com/rss.xml?q=boost%3Apopular' [1.5568 s]

该APP进行网络请求并得到响应,但没有任何响应数据。
现在,打开RWFeedViewController.m。这里有很多新代码。看看这个代码片段parseForQuery::

1
2
3
4
5
6
7
8
9
10
11
12
[self.parser parseRSSFeed:RWDeviantArtBaseURLString
parameters:[self parametersForQuery:query]
success:^(RSSChannel *channel) {
[weakSelf convertItemsPropertiesToPlainText:channel.items];
[weakSelf setFeedItems:channel.items];

[weakSelf reloadTableViewContent];
[weakSelf hideProgressHUD];
} failure:^(NSError *error) {
[weakSelf hideProgressHUD];
NSLog(@"Error: %@", error);
}];

self.parserRSSParser的实例, 它是MediaRSSParser的一部分。

这个方法初始化一个网络请求来得到RSS反馈,然后它返回一个RSSChannel成功的block。格式化后,简单的把HTML转换为纯文本,并存储channel.items设置给当前feedItems属性。
channel.items数组对象里全部包含RSSItem对象,其中每个都是RSS反馈的元素。不用我说,你也应该知道用什么在表视图里刷新了:feedItems数组!
最后,注意到项目有三个警告。WTF,too?
是哪个煞笔写了#warning出来报警告,老外就是这么调皮,做事细微,可以来我家兼职保姆了,其实这是为了提示你需要实现些代码。it’s so easy,妈妈再也不用担心我被老板加到办公室加班了。。。

创建一个自定义Cell

经过快速跳转,相信你已经找到了APP获取的数据,但没有显示任何东西。为了做到这一点,首先需要创建一个自定义的TableViewCell来显示数据。
添加一个新类到DeviantArtBrowser项目并将其命名为RWBasicCell。它是UITableViewCell的子类;确保Also create xib file没有被选中;并确保将编程语言设置为Objective-C。
打开RWBasicCell.h,并且添加一下属性:

1
2
3
4
@interface RWBasicCell : UITableViewCell

@property (nonatomic, weak) IBOutlet UILabel *titleLabel;
@property (nonatomic, weak) IBOutlet UILabel *subtitleLabel;

接下来打开Main.storyboard,拖拽一个新的UITableViewCellRWFeedViewController的表视图内。
给cell设置自定义类为RWBasicCell

设置它的**Identifier(Reuse Identifier)**为RWBasicCell

设置Row Height82

拖拽一个新的UILabel到cell里,并设置起内容为Title

给刚才添加的UILabel的Lines(显示多行的设置)设置为0,这样就可以无限制显示内容,不会出现超过范围就…了。

设置label的Preferred Widthframe值以下面的截图为准。确保Preferred WidthExplicit勾选(直到的iOS8,隐式标签宽度是不可用的,因此你需要勾选它来支持的iOS7及其更高版本)。

RWBasicCelltitleLabel接口连接起来。

接着,拖拽第二的UILabel到单元格内,贴在title label的下方,并将其文本设置为Subtitle

像之前的label一样,参照以下截图中的值更改Preferred Widthframe值。

设置subtitle label的Color值为Light Gray ColorFont值为System 15.0Lines值为0

RWBasicCellsubtitleLabel接口连接起来。

告诉我素不素so easy啊!接着我们来配置RWBasicCell的布局。现在你只需要添加自动布局约束(constraints)。选择label并且点到pin栏选择toptrailingleading,值分别为20,并确保Constrain to margins没有勾选。

无论cell大小如何,确保永远是下面这样的情况:

  • 到底部20个像素
  • 左右两边也分别是20个像素

现在选择subtitle并点到pin栏选择leadingbottomtrailing,值分别为20,再次确保Constrain to margins没有勾选。

类似title label,这些约束一起工作,以确保subtitle label总是在从底部20分,并跨越该cell的整个宽度减去一个小的填充。

提示:

这种让自动布局在一UITableViewCell内确保有足够的约束来让子视图顶到边缘 - 也就是说,每个子视图应该有上,下,左,右的约束。
此外,应该有一个明确的约束,从contentView的顶部到底部。通过这种方式,自动布局可以基于子视图正确的搞定contentView的高度。
最棘手的部分是,Interface Builder中往往不会警告你,如果你搞错某些约束条件。
当您尝试运行该项目,自动布局将根本无法返回正确的高度。例如,它可以总是返回0的高度,这是一条很好的线索,它在提示你约束可能是错的。
如果您遇到的问题在自己的项目,尝试调整的制约,直到上述条件得到满足。

选择subtitle栏,按住control并拖动title栏。选择Vertical Spacing,以subtitle栏的顶部链到title的底部。
这个过程中你可能会看到自动布局的一些警告,但你已经通过上面的步骤修复这些问题了。
选中title栏,设置其HorizontalVertical约束的优先级,Content Hugging PriorityContent Compression Resistance Priority751

选中subtitle栏,设置其HorizontalVertical约束的优先级,Content Hugging PriorityContent Compression Resistance Priority750

我们通过这样来告诉布局自动你想要的大小,以满足他们的文字多少,并且title栏约束的优先级要高于subtitle的约束(在几乎所有情况下,所有的这些限制应该由自动布局完成)。
最终,自动布局限制应该像这样的RWBasicCell:

点评:这能满足前面的自动布局标准吗?

  • 1.是否每个子视图有约束引脚?是的。
  • 2.是否有限制从顶部到底部?是。
  • *titleLabel连接顶端20像素,它连接到subtitleLabel0像素,而subtitleLabel**是20像素连接至底部。
    所以自动布局现在可以确定的单元格的高度!

接下来,你需要创建一个桥接方式从RWBasicCellDeviant Article:
选中RWBasicCell按住control并拖动到Deviant Article视图,Selection Segue方式选择Push
Interface Builder中改变cellAccessory属性,虽然说这样看起来并不太好。我们还是把Accessory改为None吧。

现在,无论用户怎么点击RWBasicCell,都会跳转到RWDetailViewController页面。
果然棒棒哒,RWBasicCell已经设置完毕!如果你现在还是RUN,你会看到什么都没有发生改变。这是What the fuck?

还记得以前的那些#warning声明吗?是的,就是那些搞你蛋的。您需要实现的表视图的数据源(data source)和委托(delegate)方法。

实现UITableView的数据源(Data source)和委托(Delegate)

RWFeedViewController.m的顶部添加这些东东:

1
2
#import "RWBasicCell.h"
static NSString * const RWBasicCellIdentifier = @"RWBasicCell";

你将在数据源和委托方法中使用RWBasicCell,这将需要你在故事板中设置RWBasicCellIdentifier重用标识符,总之,保持一致就行了。
接下来,实现数据源方法。
用下面的代码复写tableView:numberOfRowsInSection:方法:

1
2
3
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.feedItems count];
}

继续复写tableView:cellForRowAtIndexPath:方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self basicCellAtIndexPath:indexPath];
}

- (RWBasicCell *)basicCellAtIndexPath:(NSIndexPath *)indexPath {
RWBasicCell *cell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier forIndexPath:indexPath];
[self configureBasicCell:cell atIndexPath:indexPath];
return cell;
}

- (void)configureBasicCell:(RWBasicCell *)cell atIndexPath:(NSIndexPath *)indexPath {
RSSItem *item = self.feedItems[indexPath.row];
[self setTitleForCell:cell item:item];
[self setSubtitleForCell:cell item:item];
}

- (void)setTitleForCell:(RWBasicCell *)cell item:(RSSItem *)item {
NSString *title = item.title ?: NSLocalizedString(@"[No Title]", nil);
[cell.titleLabel setText:title];
}

- (void)setSubtitleForCell:(RWBasicCell *)cell item:(RSSItem *)item {
NSString *subtitle = item.mediaText ?: item.mediaDescription;

// Some subtitles can be really long, so only display the
// first 200 characters
if (subtitle.length > 200) {
subtitle = [NSString stringWithFormat:@"%@...", [subtitle substringToIndex:200]];
}

[cell.subtitleLabel setText:subtitle];
}

阐述下以上代码:

  • tableView:cellForRowAtIndexPath:代码中,调用basicCellAtIndexPath:得到一个RWBasicCell。正如你将在后面看到,这样更容易增加额外类型的自定义单元格,如果你是想创建一个辅助方法来代替数据源的方法返回单元格。
  • basicCellAtIndexPath:中,你通过configureBasicCell:atIndexPath:方法配制复用一个RWBasicCell,最后,返回配置好的单元格。
  • configureBasicCell:atIndexPath:,你得到一个item索引(indexPath)**,然后就可以获取和设置单元格上的titleLabelsubtitleLabel**文本了。

现在继续复写tableView:heightForRowAtIndexPath:及其以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self heightForBasicCellAtIndexPath:indexPath];
}

- (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath {
static RWBasicCell *sizingCell = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWBasicCellIdentifier];
});

[self configureBasicCell:sizingCell atIndexPath:indexPath];
return [self calculateHeightForConfiguredSizingCell:sizingCell];
}

- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
[sizingCell setNeedsLayout];
[sizingCell layoutIfNeeded];

CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height + 1.0f; // Add 1.0f for the cell separator height
}

终于可以使用自动布局来计算单元格高度啦!那么这里是发生了什么事情呢:
1.在tableView:heightForRowAtIndexPath:方法中,类似数据源的方法,通过heightForBasicCellAtIndexPath:方法你只是简单地做高度计算并返回,这样你方便你以后自定义单元格。
2.您可能已经注意到,heightForBasicCellAtIndexPath:很有趣。

  • 这里我们使用GCD,以确保sizingCell创建只有一次
  • configureBasicCell:atIndexPath:配制单元格
  • 实际高度是通过calculateHeightForConfiguredSizingCell:返回。你也猜到了,这样做也是为了更容易添加单元格。

3.最后,在calculateHeightForConfiguredSizingCell:中,你可以:

  • 单元格调用setNeedsLayoutlayoutIfNeeded布局其内容。
  • 通过自动布局提供的方法systemLayoutSizeFittingSize:来计算,传入参数UILayoutFittingCompressedSize,这意味着“用尽可能小的尺寸”,适合自动布局约束。
  • 返回所计算的高度加上1,这个是分割线高度。

奔跑吧兄弟,你应该能看到完整的表格了。

是不是很爽!但是你要试试横屏的话,你会发现以下奇怪的东东:

是人都会很奇怪,为啥titlesubTitle的菊花为啥会这样绽放(titlesubTitle行距太高)?

回想以下,当你同时给titlesubTitle显式(Explicit)**设置过首选宽度(Preferred Width)**。是的,这就是问题的所在。
每当屏幕方向改变,标题栏的首选宽度不更新。因此,真实高度计算就开始出错……!
这就是为什么iOS8引入了“隐性(implicit)”首选宽度,但iOS7中是不可用的,如果你要支持的iOS7以上的版本,你就不能用它。如果你想要在iOS7设备上这样搞,就等着手机玩爆破吧,煞笔。
不过幸运的是,你可以通过创建UILabel的子类来解决这个问题。添加一个新类到项目,将其命名为RWLabel并使它继承自UILabel。

RWLabel.m中,复写@implementation RWLabel下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];

// If this is a multiline label, need to make sure
// preferredMaxLayoutWidth always matches the frame width
// (i.e. orientation change can mess this up)

if (self.numberOfLines == 0 && bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
[self setNeedsUpdateConstraints];
}
}

正如代码所写,RWLabel可以确保preferredMaxLayoutWidth总是等于bound的宽度。

接下来,你需要更新RWBasicCell代码,打开RWBasicCell.h,接着导入头文件:

1
#import "RWLabel.h"

接着把属性什么替换成下面这样:

1
2
@property (nonatomic, weak) IBOutlet RWLabel *titleLabel;
@property (nonatomic, weak) IBOutlet RWLabel *subtitleLabel;

继续打开Main.storyboard,选到Feed View Controller, 把两个标题栏的class都改为RWLabel

打开RWFeedViewController.m,在calculateHeightForConfiguredSizingCell:方法的最前面添加以下代码:

1
sizingCell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.tableView.frame), CGRectGetHeight(sizingCell.bounds));

这样做是为了让RWLabel更新preferredMaxLayoutWidth属性。
再次跑起来吧,这下无论横屏还是竖屏,显示都妥妥的了。

说好的图片呢?

一个好的APP,我们是不是应该加点图片进去呢?

未完待续…